{
 "cells": [
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<font color=\"red\">注</font>: 使用 tensorboard 可视化需要安装 tensorflow (TensorBoard依赖于tensorflow库，可以任意安装tensorflow的gpu/cpu版本)\n",
    "\n",
    "```shell\n",
    "pip install tensorflow-cpu\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-31T14:46:47.684968Z",
     "start_time": "2025-01-31T14:46:43.938471Z"
    }
   },
   "source": [
    "import matplotlib as mpl\n",
    "import matplotlib.pyplot as plt\n",
    "%matplotlib inline\n",
    "import numpy as np\n",
    "import sklearn\n",
    "import pandas as pd\n",
    "import os\n",
    "import sys\n",
    "import time\n",
    "from tqdm.auto import tqdm\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.nn.functional as F\n",
    "\n",
    "print(sys.version_info)\n",
    "for module in mpl, np, pd, sklearn, torch:\n",
    "    print(module.__name__, module.__version__)\n",
    "    \n",
    "device = torch.device(\"cuda:0\") if torch.cuda.is_available() else torch.device(\"cpu\")\n",
    "print(device)\n",
    "\n",
    "seed = 42\n"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sys.version_info(major=3, minor=12, micro=3, releaselevel='final', serial=0)\n",
      "matplotlib 3.10.0\n",
      "numpy 1.26.4\n",
      "pandas 2.2.3\n",
      "sklearn 1.6.1\n",
      "torch 2.5.1+cpu\n",
      "cpu\n"
     ]
    }
   ],
   "execution_count": 1
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 数据准备\n",
    "\n",
    "```shell\n",
    "$ tree -L 2 archive \n",
    "archive\n",
    "├── monkey_labels.txt\n",
    "├── training\n",
    "│   ├── n0\n",
    "│   ├── n1\n",
    "│   ├── n2\n",
    "│   ├── n3\n",
    "│   ├── n4\n",
    "│   ├── n5\n",
    "│   ├── n6\n",
    "│   ├── n7\n",
    "│   ├── n8\n",
    "│   └── n9\n",
    "└── validation\n",
    "    ├── n0\n",
    "    ├── n1\n",
    "    ├── n2\n",
    "    ├── n3\n",
    "    ├── n4\n",
    "    ├── n5\n",
    "    ├── n6\n",
    "    ├── n7\n",
    "    ├── n8\n",
    "    └── n9\n",
    "\n",
    "22 directories, 1 file\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-31T14:47:14.504639Z",
     "start_time": "2025-01-31T14:47:12.788905Z"
    }
   },
   "source": [
    "from torchvision import datasets\n",
    "from torchvision.transforms import ToTensor, Resize, Compose, ConvertImageDtype, Normalize\n",
    "\n",
    "\n",
    "from pathlib import Path\n",
    "\n",
    "DATA_DIR = Path(\"./archive/\")\n",
    "\n",
    "class MonkeyDataset(datasets.ImageFolder):\n",
    "    def __init__(self, mode, transform=None):\n",
    "        if mode == \"train\":\n",
    "            root = DATA_DIR / \"training\"\n",
    "        elif mode == \"val\":\n",
    "            root = DATA_DIR / \"validation\"\n",
    "        else:\n",
    "            raise ValueError(\"mode should be one of the following: train, val, but got {}\".format(mode))\n",
    "        super().__init__(root, transform)\n",
    "        self.imgs = self.samples\n",
    "        self.targets = [s[1] for s in self.samples]\n",
    "\n",
    "# resnet 要求的，见 https://pytorch.org/vision/stable/models/generated/torchvision.models.resnet50.html\n",
    "img_h, img_w = 224, 224\n",
    "transform = Compose([\n",
    "     Resize((img_h, img_w)),\n",
    "     ToTensor(),\n",
    "     Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),\n",
    "     ConvertImageDtype(torch.float),\n",
    "])\n",
    "\n",
    "\n",
    "train_ds = MonkeyDataset(\"train\", transform=transform)\n",
    "val_ds = MonkeyDataset(\"val\", transform=transform)\n",
    "\n",
    "print(\"load {} images from training dataset\".format(len(train_ds)))\n",
    "print(\"load {} images from validation dataset\".format(len(val_ds)))"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "load 1097 images from training dataset\n",
      "load 272 images from validation dataset\n"
     ]
    }
   ],
   "execution_count": 2
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-31T14:47:14.509410Z",
     "start_time": "2025-01-31T14:47:14.505635Z"
    }
   },
   "source": [
    "# 数据类别\n",
    "train_ds.classes, train_ds.class_to_idx"
   ],
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(['n0', 'n1', 'n2', 'n3', 'n4', 'n5', 'n6', 'n7', 'n8', 'n9'],\n",
       " {'n0': 0,\n",
       "  'n1': 1,\n",
       "  'n2': 2,\n",
       "  'n3': 3,\n",
       "  'n4': 4,\n",
       "  'n5': 5,\n",
       "  'n6': 6,\n",
       "  'n7': 7,\n",
       "  'n8': 8,\n",
       "  'n9': 9})"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 3
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-31T14:47:22.810443Z",
     "start_time": "2025-01-31T14:47:22.795016Z"
    }
   },
   "source": [
    "# 图片路径 及 标签\n",
    "for fpath, label in train_ds.imgs:\n",
    "    print(fpath, label)\n",
    "    break\n",
    "\n",
    "for img, label in train_ds:\n",
    "    # c, h, w  label\n",
    "    print(img.shape, label)\n",
    "    break"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "archive\\training\\n0\\n0018.jpg 0\n",
      "torch.Size([3, 224, 224]) 0\n"
     ]
    }
   ],
   "execution_count": 4
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-31T14:47:42.414234Z",
     "start_time": "2025-01-31T14:47:42.409341Z"
    }
   },
   "source": [
    "# 遍历train_ds得到每张图片，计算每个通道的均值和方差\n",
    "def cal_mean_std(ds):\n",
    "    mean = 0.\n",
    "    std = 0.\n",
    "    for img, _ in ds:\n",
    "        mean += img.mean(dim=(1, 2))\n",
    "        std += img.std(dim=(1, 2))\n",
    "    mean /= len(ds)\n",
    "    std /= len(ds)\n",
    "    return mean, std\n",
    "\n",
    "# 经过 normalize 后 均值为0，方差为1\n",
    "# print(cal_mean_std(train_ds))"
   ],
   "outputs": [],
   "execution_count": 5
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-31T14:47:49.896814Z",
     "start_time": "2025-01-31T14:47:49.892575Z"
    }
   },
   "source": [
    "import torch.nn as nn\n",
    "from torch.utils.data.dataloader import DataLoader    \n",
    "\n",
    "batch_size = 16\n",
    "# 从数据集到dataloader\n",
    "train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True)\n",
    "val_loader = DataLoader(val_ds, batch_size=batch_size, shuffle=False)"
   ],
   "outputs": [],
   "execution_count": 6
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-31T14:47:51.622440Z",
     "start_time": "2025-01-31T14:47:51.350341Z"
    }
   },
   "source": [
    "for imgs, labels in train_loader:\n",
    "    print(imgs.shape)\n",
    "    print(labels.shape)\n",
    "    break"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "torch.Size([16, 3, 224, 224])\n",
      "torch.Size([16])\n"
     ]
    }
   ],
   "execution_count": 7
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 定义模型"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-31T14:49:40.572896Z",
     "start_time": "2025-01-31T14:49:29.070565Z"
    }
   },
   "source": [
    "from torchvision.models import resnet50\n",
    "\n",
    "\n",
    "# 加载resnet50模型以及预训练权重，冻结除最后一层外所有层的权重，去除最后一层并添加自定义分类层\n",
    "class ResNet50(nn.Module):\n",
    "    def __init__(self, num_classes=10, frozen=True):\n",
    "        super().__init__()\n",
    "        # 下载预训练权重\n",
    "        self.model = resnet50(weights=\"IMAGENET1K_V2\",) # 这里的weights参数可以选择\"IMAGENET1K_V2\"或None，None表示随机初始化\n",
    "        # 冻结前面的层\n",
    "        if frozen:\n",
    "            for param in self.model.parameters():\n",
    "                param.requires_grad = False # 冻结权重,只对最后一层fc进行训练\n",
    "        # 解冻某层\n",
    "        # for param in self.model.fc.parameters():\n",
    "        #     if name == \"layer4.2.conv3.weight\":\n",
    "        #         param.requires_grad = True # 解冻某层权重\n",
    "        # 添加自定义分类层\n",
    "        # print(self.model)\n",
    "        print(self.model.fc.in_features) # 打印resnet50的最后一层的输入通道数\n",
    "        print(self.model.fc.out_features) # 打印resnet50的最后一层的输出通道数 1000\n",
    "        self.model.fc = nn.Linear(self.model.fc.in_features, num_classes)   # 自定义分类层,把resnet50的最后一层改为num_classes个输出\n",
    "        \n",
    "        \n",
    "    def forward(self, x):\n",
    "        return self.model(x)\n",
    "\n",
    "\n",
    "for idx, (key, value) in enumerate(ResNet50().named_parameters()):\n",
    "    print(f\"{key:^40}paramerters num: {np.prod(value.shape)}\")\n"
   ],
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Downloading: \"https://download.pytorch.org/models/resnet50-11ad3fa6.pth\" to C:\\Users\\29470/.cache\\torch\\hub\\checkpoints\\resnet50-11ad3fa6.pth\n",
      "100%|██████████| 97.8M/97.8M [00:10<00:00, 9.66MB/s]\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2048\n",
      "1000\n",
      "           model.conv1.weight           paramerters num: 9408\n",
      "            model.bn1.weight            paramerters num: 64\n",
      "             model.bn1.bias             paramerters num: 64\n",
      "      model.layer1.0.conv1.weight       paramerters num: 4096\n",
      "       model.layer1.0.bn1.weight        paramerters num: 64\n",
      "        model.layer1.0.bn1.bias         paramerters num: 64\n",
      "      model.layer1.0.conv2.weight       paramerters num: 36864\n",
      "       model.layer1.0.bn2.weight        paramerters num: 64\n",
      "        model.layer1.0.bn2.bias         paramerters num: 64\n",
      "      model.layer1.0.conv3.weight       paramerters num: 16384\n",
      "       model.layer1.0.bn3.weight        paramerters num: 256\n",
      "        model.layer1.0.bn3.bias         paramerters num: 256\n",
      "   model.layer1.0.downsample.0.weight   paramerters num: 16384\n",
      "   model.layer1.0.downsample.1.weight   paramerters num: 256\n",
      "    model.layer1.0.downsample.1.bias    paramerters num: 256\n",
      "      model.layer1.1.conv1.weight       paramerters num: 16384\n",
      "       model.layer1.1.bn1.weight        paramerters num: 64\n",
      "        model.layer1.1.bn1.bias         paramerters num: 64\n",
      "      model.layer1.1.conv2.weight       paramerters num: 36864\n",
      "       model.layer1.1.bn2.weight        paramerters num: 64\n",
      "        model.layer1.1.bn2.bias         paramerters num: 64\n",
      "      model.layer1.1.conv3.weight       paramerters num: 16384\n",
      "       model.layer1.1.bn3.weight        paramerters num: 256\n",
      "        model.layer1.1.bn3.bias         paramerters num: 256\n",
      "      model.layer1.2.conv1.weight       paramerters num: 16384\n",
      "       model.layer1.2.bn1.weight        paramerters num: 64\n",
      "        model.layer1.2.bn1.bias         paramerters num: 64\n",
      "      model.layer1.2.conv2.weight       paramerters num: 36864\n",
      "       model.layer1.2.bn2.weight        paramerters num: 64\n",
      "        model.layer1.2.bn2.bias         paramerters num: 64\n",
      "      model.layer1.2.conv3.weight       paramerters num: 16384\n",
      "       model.layer1.2.bn3.weight        paramerters num: 256\n",
      "        model.layer1.2.bn3.bias         paramerters num: 256\n",
      "      model.layer2.0.conv1.weight       paramerters num: 32768\n",
      "       model.layer2.0.bn1.weight        paramerters num: 128\n",
      "        model.layer2.0.bn1.bias         paramerters num: 128\n",
      "      model.layer2.0.conv2.weight       paramerters num: 147456\n",
      "       model.layer2.0.bn2.weight        paramerters num: 128\n",
      "        model.layer2.0.bn2.bias         paramerters num: 128\n",
      "      model.layer2.0.conv3.weight       paramerters num: 65536\n",
      "       model.layer2.0.bn3.weight        paramerters num: 512\n",
      "        model.layer2.0.bn3.bias         paramerters num: 512\n",
      "   model.layer2.0.downsample.0.weight   paramerters num: 131072\n",
      "   model.layer2.0.downsample.1.weight   paramerters num: 512\n",
      "    model.layer2.0.downsample.1.bias    paramerters num: 512\n",
      "      model.layer2.1.conv1.weight       paramerters num: 65536\n",
      "       model.layer2.1.bn1.weight        paramerters num: 128\n",
      "        model.layer2.1.bn1.bias         paramerters num: 128\n",
      "      model.layer2.1.conv2.weight       paramerters num: 147456\n",
      "       model.layer2.1.bn2.weight        paramerters num: 128\n",
      "        model.layer2.1.bn2.bias         paramerters num: 128\n",
      "      model.layer2.1.conv3.weight       paramerters num: 65536\n",
      "       model.layer2.1.bn3.weight        paramerters num: 512\n",
      "        model.layer2.1.bn3.bias         paramerters num: 512\n",
      "      model.layer2.2.conv1.weight       paramerters num: 65536\n",
      "       model.layer2.2.bn1.weight        paramerters num: 128\n",
      "        model.layer2.2.bn1.bias         paramerters num: 128\n",
      "      model.layer2.2.conv2.weight       paramerters num: 147456\n",
      "       model.layer2.2.bn2.weight        paramerters num: 128\n",
      "        model.layer2.2.bn2.bias         paramerters num: 128\n",
      "      model.layer2.2.conv3.weight       paramerters num: 65536\n",
      "       model.layer2.2.bn3.weight        paramerters num: 512\n",
      "        model.layer2.2.bn3.bias         paramerters num: 512\n",
      "      model.layer2.3.conv1.weight       paramerters num: 65536\n",
      "       model.layer2.3.bn1.weight        paramerters num: 128\n",
      "        model.layer2.3.bn1.bias         paramerters num: 128\n",
      "      model.layer2.3.conv2.weight       paramerters num: 147456\n",
      "       model.layer2.3.bn2.weight        paramerters num: 128\n",
      "        model.layer2.3.bn2.bias         paramerters num: 128\n",
      "      model.layer2.3.conv3.weight       paramerters num: 65536\n",
      "       model.layer2.3.bn3.weight        paramerters num: 512\n",
      "        model.layer2.3.bn3.bias         paramerters num: 512\n",
      "      model.layer3.0.conv1.weight       paramerters num: 131072\n",
      "       model.layer3.0.bn1.weight        paramerters num: 256\n",
      "        model.layer3.0.bn1.bias         paramerters num: 256\n",
      "      model.layer3.0.conv2.weight       paramerters num: 589824\n",
      "       model.layer3.0.bn2.weight        paramerters num: 256\n",
      "        model.layer3.0.bn2.bias         paramerters num: 256\n",
      "      model.layer3.0.conv3.weight       paramerters num: 262144\n",
      "       model.layer3.0.bn3.weight        paramerters num: 1024\n",
      "        model.layer3.0.bn3.bias         paramerters num: 1024\n",
      "   model.layer3.0.downsample.0.weight   paramerters num: 524288\n",
      "   model.layer3.0.downsample.1.weight   paramerters num: 1024\n",
      "    model.layer3.0.downsample.1.bias    paramerters num: 1024\n",
      "      model.layer3.1.conv1.weight       paramerters num: 262144\n",
      "       model.layer3.1.bn1.weight        paramerters num: 256\n",
      "        model.layer3.1.bn1.bias         paramerters num: 256\n",
      "      model.layer3.1.conv2.weight       paramerters num: 589824\n",
      "       model.layer3.1.bn2.weight        paramerters num: 256\n",
      "        model.layer3.1.bn2.bias         paramerters num: 256\n",
      "      model.layer3.1.conv3.weight       paramerters num: 262144\n",
      "       model.layer3.1.bn3.weight        paramerters num: 1024\n",
      "        model.layer3.1.bn3.bias         paramerters num: 1024\n",
      "      model.layer3.2.conv1.weight       paramerters num: 262144\n",
      "       model.layer3.2.bn1.weight        paramerters num: 256\n",
      "        model.layer3.2.bn1.bias         paramerters num: 256\n",
      "      model.layer3.2.conv2.weight       paramerters num: 589824\n",
      "       model.layer3.2.bn2.weight        paramerters num: 256\n",
      "        model.layer3.2.bn2.bias         paramerters num: 256\n",
      "      model.layer3.2.conv3.weight       paramerters num: 262144\n",
      "       model.layer3.2.bn3.weight        paramerters num: 1024\n",
      "        model.layer3.2.bn3.bias         paramerters num: 1024\n",
      "      model.layer3.3.conv1.weight       paramerters num: 262144\n",
      "       model.layer3.3.bn1.weight        paramerters num: 256\n",
      "        model.layer3.3.bn1.bias         paramerters num: 256\n",
      "      model.layer3.3.conv2.weight       paramerters num: 589824\n",
      "       model.layer3.3.bn2.weight        paramerters num: 256\n",
      "        model.layer3.3.bn2.bias         paramerters num: 256\n",
      "      model.layer3.3.conv3.weight       paramerters num: 262144\n",
      "       model.layer3.3.bn3.weight        paramerters num: 1024\n",
      "        model.layer3.3.bn3.bias         paramerters num: 1024\n",
      "      model.layer3.4.conv1.weight       paramerters num: 262144\n",
      "       model.layer3.4.bn1.weight        paramerters num: 256\n",
      "        model.layer3.4.bn1.bias         paramerters num: 256\n",
      "      model.layer3.4.conv2.weight       paramerters num: 589824\n",
      "       model.layer3.4.bn2.weight        paramerters num: 256\n",
      "        model.layer3.4.bn2.bias         paramerters num: 256\n",
      "      model.layer3.4.conv3.weight       paramerters num: 262144\n",
      "       model.layer3.4.bn3.weight        paramerters num: 1024\n",
      "        model.layer3.4.bn3.bias         paramerters num: 1024\n",
      "      model.layer3.5.conv1.weight       paramerters num: 262144\n",
      "       model.layer3.5.bn1.weight        paramerters num: 256\n",
      "        model.layer3.5.bn1.bias         paramerters num: 256\n",
      "      model.layer3.5.conv2.weight       paramerters num: 589824\n",
      "       model.layer3.5.bn2.weight        paramerters num: 256\n",
      "        model.layer3.5.bn2.bias         paramerters num: 256\n",
      "      model.layer3.5.conv3.weight       paramerters num: 262144\n",
      "       model.layer3.5.bn3.weight        paramerters num: 1024\n",
      "        model.layer3.5.bn3.bias         paramerters num: 1024\n",
      "      model.layer4.0.conv1.weight       paramerters num: 524288\n",
      "       model.layer4.0.bn1.weight        paramerters num: 512\n",
      "        model.layer4.0.bn1.bias         paramerters num: 512\n",
      "      model.layer4.0.conv2.weight       paramerters num: 2359296\n",
      "       model.layer4.0.bn2.weight        paramerters num: 512\n",
      "        model.layer4.0.bn2.bias         paramerters num: 512\n",
      "      model.layer4.0.conv3.weight       paramerters num: 1048576\n",
      "       model.layer4.0.bn3.weight        paramerters num: 2048\n",
      "        model.layer4.0.bn3.bias         paramerters num: 2048\n",
      "   model.layer4.0.downsample.0.weight   paramerters num: 2097152\n",
      "   model.layer4.0.downsample.1.weight   paramerters num: 2048\n",
      "    model.layer4.0.downsample.1.bias    paramerters num: 2048\n",
      "      model.layer4.1.conv1.weight       paramerters num: 1048576\n",
      "       model.layer4.1.bn1.weight        paramerters num: 512\n",
      "        model.layer4.1.bn1.bias         paramerters num: 512\n",
      "      model.layer4.1.conv2.weight       paramerters num: 2359296\n",
      "       model.layer4.1.bn2.weight        paramerters num: 512\n",
      "        model.layer4.1.bn2.bias         paramerters num: 512\n",
      "      model.layer4.1.conv3.weight       paramerters num: 1048576\n",
      "       model.layer4.1.bn3.weight        paramerters num: 2048\n",
      "        model.layer4.1.bn3.bias         paramerters num: 2048\n",
      "      model.layer4.2.conv1.weight       paramerters num: 1048576\n",
      "       model.layer4.2.bn1.weight        paramerters num: 512\n",
      "        model.layer4.2.bn1.bias         paramerters num: 512\n",
      "      model.layer4.2.conv2.weight       paramerters num: 2359296\n",
      "       model.layer4.2.bn2.weight        paramerters num: 512\n",
      "        model.layer4.2.bn2.bias         paramerters num: 512\n",
      "      model.layer4.2.conv3.weight       paramerters num: 1048576\n",
      "       model.layer4.2.bn3.weight        paramerters num: 2048\n",
      "        model.layer4.2.bn3.bias         paramerters num: 2048\n",
      "            model.fc.weight             paramerters num: 20480\n",
      "             model.fc.bias              paramerters num: 10\n"
     ]
    }
   ],
   "execution_count": 8
  },
  {
   "cell_type": "code",
   "source": [
    "model = ResNet50(num_classes=10, frozen=True)"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-01-31T14:51:51.608031Z",
     "start_time": "2025-01-31T14:51:51.343455Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2048\n",
      "1000\n"
     ]
    }
   ],
   "execution_count": 9
  },
  {
   "cell_type": "code",
   "source": [
    "model"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-01-31T14:52:26.871879Z",
     "start_time": "2025-01-31T14:52:26.866854Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "ResNet50(\n",
       "  (model): ResNet(\n",
       "    (conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)\n",
       "    (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "    (relu): ReLU(inplace=True)\n",
       "    (maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)\n",
       "    (layer1): Sequential(\n",
       "      (0): Bottleneck(\n",
       "        (conv1): Conv2d(64, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "        (downsample): Sequential(\n",
       "          (0): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "          (1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        )\n",
       "      )\n",
       "      (1): Bottleneck(\n",
       "        (conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (2): Bottleneck(\n",
       "        (conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "    )\n",
       "    (layer2): Sequential(\n",
       "      (0): Bottleneck(\n",
       "        (conv1): Conv2d(256, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "        (downsample): Sequential(\n",
       "          (0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)\n",
       "          (1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        )\n",
       "      )\n",
       "      (1): Bottleneck(\n",
       "        (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (2): Bottleneck(\n",
       "        (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (3): Bottleneck(\n",
       "        (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "    )\n",
       "    (layer3): Sequential(\n",
       "      (0): Bottleneck(\n",
       "        (conv1): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "        (downsample): Sequential(\n",
       "          (0): Conv2d(512, 1024, kernel_size=(1, 1), stride=(2, 2), bias=False)\n",
       "          (1): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        )\n",
       "      )\n",
       "      (1): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (2): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (3): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (4): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (5): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "    )\n",
       "    (layer4): Sequential(\n",
       "      (0): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "        (downsample): Sequential(\n",
       "          (0): Conv2d(1024, 2048, kernel_size=(1, 1), stride=(2, 2), bias=False)\n",
       "          (1): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        )\n",
       "      )\n",
       "      (1): Bottleneck(\n",
       "        (conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (2): Bottleneck(\n",
       "        (conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "    )\n",
       "    (avgpool): AdaptiveAvgPool2d(output_size=(1, 1))\n",
       "    (fc): Linear(in_features=2048, out_features=10, bias=True)\n",
       "  )\n",
       ")"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 10
  },
  {
   "cell_type": "code",
   "source": [
    "total_params = sum(p.numel() for p in model.parameters() )\n",
    "print(f\"Total trainable parameters: {total_params}\")"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-01-31T14:52:45.374470Z",
     "start_time": "2025-01-31T14:52:45.370717Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Total trainable parameters: 23528522\n"
     ]
    }
   ],
   "execution_count": 11
  },
  {
   "cell_type": "code",
   "source": [
    "m = nn.AdaptiveAvgPool2d(output_size=(1, 1))  # 自适应平均值池化层\n",
    "input = torch.randn(1, 2048, 9, 9)  # 不管输出尺寸是多少，都变成1*1的输出\n",
    "output = m(input)\n",
    "output.shape"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-01-31T14:54:44.782477Z",
     "start_time": "2025-01-31T14:54:44.775803Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "torch.Size([1, 2048, 1, 1])"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 12
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "outputs": [
    {
     "data": {
      "text/plain": "2359296"
     },
     "execution_count": 29,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "512*3*3*512"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2024-07-23T07:06:55.092794100Z",
     "start_time": "2024-07-23T07:06:55.088043800Z"
    }
   }
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "outputs": [
    {
     "data": {
      "text/plain": "1048576"
     },
     "execution_count": 27,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "512*1*1*2048"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2024-07-23T03:45:55.270615100Z",
     "start_time": "2024-07-23T03:45:55.261439200Z"
    }
   }
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "outputs": [
    {
     "data": {
      "text/plain": "32.0"
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "512/16"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2024-07-23T02:56:07.349270600Z",
     "start_time": "2024-07-23T02:56:07.337722300Z"
    }
   }
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 训练\n",
    "\n",
    "pytorch的训练需要自行实现，包括\n",
    "1. 定义损失函数\n",
    "2. 定义优化器\n",
    "3. 定义训练步\n",
    "4. 训练"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-31T15:00:40.531679Z",
     "start_time": "2025-01-31T15:00:40.483651Z"
    }
   },
   "source": [
    "from sklearn.metrics import accuracy_score\n",
    "\n",
    "@torch.no_grad()\n",
    "def evaluating(model, dataloader, loss_fct):\n",
    "    loss_list = []\n",
    "    pred_list = []\n",
    "    label_list = []\n",
    "    for datas, labels in dataloader:\n",
    "        datas = datas.to(device)\n",
    "        labels = labels.to(device)\n",
    "        # 前向计算\n",
    "        logits = model(datas)\n",
    "        loss = loss_fct(logits, labels)         # 验证集损失\n",
    "        loss_list.append(loss.item())\n",
    "        \n",
    "        preds = logits.argmax(axis=-1)    # 验证集预测\n",
    "        pred_list.extend(preds.cpu().numpy().tolist())\n",
    "        label_list.extend(labels.cpu().numpy().tolist())\n",
    "        \n",
    "    acc = accuracy_score(label_list, pred_list)\n",
    "    return np.mean(loss_list), acc\n"
   ],
   "outputs": [],
   "execution_count": 13
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### TensorBoard 可视化\n",
    "\n",
    "\n",
    "训练过程中可以使用如下命令启动tensorboard服务。\n",
    "\n",
    "```shell\n",
    "tensorboard \\\n",
    "    --logdir=runs \\     # log 存放路径\n",
    "    --host 0.0.0.0 \\    # ip\n",
    "    --port 8848         # 端口\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-31T15:00:44.398357Z",
     "start_time": "2025-01-31T15:00:41.381502Z"
    }
   },
   "source": [
    "from torch.utils.tensorboard import SummaryWriter\n",
    "\n",
    "\n",
    "class TensorBoardCallback:\n",
    "    def __init__(self, log_dir, flush_secs=10):\n",
    "        \"\"\"\n",
    "        Args:\n",
    "            log_dir (str): dir to write log.\n",
    "            flush_secs (int, optional): write to dsk each flush_secs seconds. Defaults to 10.\n",
    "        \"\"\"\n",
    "        self.writer = SummaryWriter(log_dir=log_dir, flush_secs=flush_secs)\n",
    "\n",
    "    def draw_model(self, model, input_shape):\n",
    "        self.writer.add_graph(model, input_to_model=torch.randn(input_shape))\n",
    "        \n",
    "    def add_loss_scalars(self, step, loss, val_loss):\n",
    "        self.writer.add_scalars(\n",
    "            main_tag=\"training/loss\", \n",
    "            tag_scalar_dict={\"loss\": loss, \"val_loss\": val_loss},\n",
    "            global_step=step,\n",
    "            )\n",
    "        \n",
    "    def add_acc_scalars(self, step, acc, val_acc):\n",
    "        self.writer.add_scalars(\n",
    "            main_tag=\"training/accuracy\",\n",
    "            tag_scalar_dict={\"accuracy\": acc, \"val_accuracy\": val_acc},\n",
    "            global_step=step,\n",
    "        )\n",
    "        \n",
    "    def add_lr_scalars(self, step, learning_rate):\n",
    "        self.writer.add_scalars(\n",
    "            main_tag=\"training/learning_rate\",\n",
    "            tag_scalar_dict={\"learning_rate\": learning_rate},\n",
    "            global_step=step,\n",
    "            \n",
    "        )\n",
    "    \n",
    "    def __call__(self, step, **kwargs):\n",
    "        # add loss\n",
    "        loss = kwargs.pop(\"loss\", None)\n",
    "        val_loss = kwargs.pop(\"val_loss\", None)\n",
    "        if loss is not None and val_loss is not None:\n",
    "            self.add_loss_scalars(step, loss, val_loss)\n",
    "        # add acc\n",
    "        acc = kwargs.pop(\"acc\", None)\n",
    "        val_acc = kwargs.pop(\"val_acc\", None)\n",
    "        if acc is not None and val_acc is not None:\n",
    "            self.add_acc_scalars(step, acc, val_acc)\n",
    "        # add lr\n",
    "        learning_rate = kwargs.pop(\"lr\", None)\n",
    "        if learning_rate is not None:\n",
    "            self.add_lr_scalars(step, learning_rate)\n"
   ],
   "outputs": [],
   "execution_count": 14
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Save Best\n"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-31T15:00:50.779772Z",
     "start_time": "2025-01-31T15:00:50.775254Z"
    }
   },
   "source": [
    "class SaveCheckpointsCallback:\n",
    "    def __init__(self, save_dir, save_step=5000, save_best_only=True):\n",
    "        \"\"\"\n",
    "        Save checkpoints each save_epoch epoch. \n",
    "        We save checkpoint by epoch in this implementation.\n",
    "        Usually, training scripts with pytorch evaluating model and save checkpoint by step.\n",
    "\n",
    "        Args:\n",
    "            save_dir (str): dir to save checkpoint\n",
    "            save_epoch (int, optional): the frequency to save checkpoint. Defaults to 1.\n",
    "            save_best_only (bool, optional): If True, only save the best model or save each model at every epoch.\n",
    "        \"\"\"\n",
    "        self.save_dir = save_dir\n",
    "        self.save_step = save_step\n",
    "        self.save_best_only = save_best_only\n",
    "        self.best_metrics = -1\n",
    "        \n",
    "        # mkdir\n",
    "        if not os.path.exists(self.save_dir):\n",
    "            os.mkdir(self.save_dir)\n",
    "        \n",
    "    def __call__(self, step, state_dict, metric=None):\n",
    "        if step % self.save_step > 0:\n",
    "            return\n",
    "        \n",
    "        if self.save_best_only:\n",
    "            assert metric is not None\n",
    "            if metric >= self.best_metrics:\n",
    "                # save checkpoints\n",
    "                torch.save(state_dict, os.path.join(self.save_dir, \"best.ckpt\"))\n",
    "                # update best metrics\n",
    "                self.best_metrics = metric\n",
    "        else:\n",
    "            torch.save(state_dict, os.path.join(self.save_dir, f\"{step}.ckpt\"))"
   ],
   "outputs": [],
   "execution_count": 15
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Early Stop"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-31T15:00:54.623329Z",
     "start_time": "2025-01-31T15:00:54.618823Z"
    }
   },
   "source": [
    "class EarlyStopCallback:\n",
    "    def __init__(self, patience=5, min_delta=0.01):\n",
    "        \"\"\"\n",
    "\n",
    "        Args:\n",
    "            patience (int, optional): Number of epochs with no improvement after which training will be stopped.. Defaults to 5.\n",
    "            min_delta (float, optional): Minimum change in the monitored quantity to qualify as an improvement, i.e. an absolute \n",
    "                change of less than min_delta, will count as no improvement. Defaults to 0.01.\n",
    "        \"\"\"\n",
    "        self.patience = patience\n",
    "        self.min_delta = min_delta\n",
    "        self.best_metric = -1\n",
    "        self.counter = 0\n",
    "        \n",
    "    def __call__(self, metric):\n",
    "        if metric >= self.best_metric + self.min_delta:\n",
    "            # update best metric\n",
    "            self.best_metric = metric\n",
    "            # reset counter \n",
    "            self.counter = 0\n",
    "        else: \n",
    "            self.counter += 1\n",
    "            \n",
    "    @property\n",
    "    def early_stop(self):\n",
    "        return self.counter >= self.patience"
   ],
   "outputs": [],
   "execution_count": 16
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-31T15:10:07.568601Z",
     "start_time": "2025-01-31T15:01:01.729546Z"
    }
   },
   "source": [
    "# 训练\n",
    "def training(\n",
    "    model, \n",
    "    train_loader, \n",
    "    val_loader, \n",
    "    epoch, \n",
    "    loss_fct, \n",
    "    optimizer, \n",
    "    tensorboard_callback=None,\n",
    "    save_ckpt_callback=None,\n",
    "    early_stop_callback=None,\n",
    "    eval_step=500,\n",
    "    ):\n",
    "    record_dict = {\n",
    "        \"train\": [],\n",
    "        \"val\": []\n",
    "    }\n",
    "    \n",
    "    global_step = 0\n",
    "    model.train()\n",
    "    with tqdm(total=epoch * len(train_loader)) as pbar:\n",
    "        for epoch_id in range(epoch):\n",
    "            # training\n",
    "            for datas, labels in train_loader:\n",
    "                datas = datas.to(device)\n",
    "                labels = labels.to(device)\n",
    "                # 梯度清空\n",
    "                optimizer.zero_grad()\n",
    "                # 模型前向计算\n",
    "                logits = model(datas)\n",
    "                # 计算损失\n",
    "                loss = loss_fct(logits, labels)\n",
    "                # 梯度回传\n",
    "                loss.backward()\n",
    "                # 调整优化器，包括学习率的变动等\n",
    "                optimizer.step()\n",
    "                preds = logits.argmax(axis=-1)\n",
    "            \n",
    "                acc = accuracy_score(labels.cpu().numpy(), preds.cpu().numpy())    \n",
    "                loss = loss.cpu().item()\n",
    "                # record\n",
    "                \n",
    "                record_dict[\"train\"].append({\n",
    "                    \"loss\": loss, \"acc\": acc, \"step\": global_step\n",
    "                })\n",
    "                \n",
    "                # evaluating\n",
    "                if global_step % eval_step == 0:\n",
    "                    model.eval()\n",
    "                    val_loss, val_acc = evaluating(model, val_loader, loss_fct)\n",
    "                    record_dict[\"val\"].append({\n",
    "                        \"loss\": val_loss, \"acc\": val_acc, \"step\": global_step\n",
    "                    })\n",
    "                    model.train()\n",
    "                    \n",
    "                    # 1. 使用 tensorboard 可视化\n",
    "                    if tensorboard_callback is not None:\n",
    "                        tensorboard_callback(\n",
    "                            global_step, \n",
    "                            loss=loss, val_loss=val_loss,\n",
    "                            acc=acc, val_acc=val_acc,\n",
    "                            lr=optimizer.param_groups[0][\"lr\"],\n",
    "                            )\n",
    "                \n",
    "                    # 2. 保存模型权重 save model checkpoint\n",
    "                    if save_ckpt_callback is not None:\n",
    "                        save_ckpt_callback(global_step, model.state_dict(), metric=val_acc)\n",
    "\n",
    "                    # 3. 早停 Early Stop\n",
    "                    if early_stop_callback is not None:\n",
    "                        early_stop_callback(val_acc)\n",
    "                        if early_stop_callback.early_stop:\n",
    "                            print(f\"Early stop at epoch {epoch_id} / global_step {global_step}\")\n",
    "                            return record_dict\n",
    "                    \n",
    "                # udate step\n",
    "                global_step += 1\n",
    "                pbar.update(1)\n",
    "                pbar.set_postfix({\"epoch\": epoch_id})\n",
    "        \n",
    "    return record_dict\n",
    "        \n",
    "\n",
    "epoch = 20\n",
    "\n",
    "model = ResNet50(num_classes=10)\n",
    "\n",
    "# 1. 定义损失函数 采用交叉熵损失\n",
    "loss_fct = nn.CrossEntropyLoss()\n",
    "# 2. 定义优化器 采用 sgd\n",
    "# Optimizers specified in the torch.optim package\n",
    "optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.0)\n",
    "\n",
    "# 1. tensorboard 可视化\n",
    "if not os.path.exists(\"runs\"):\n",
    "    os.mkdir(\"runs\")\n",
    "# tensorboard_callback = TensorBoardCallback(\"runs/monkeys-resnet50\")\n",
    "# tensorboard_callback.draw_model(model, [1, 3, img_h, img_w])\n",
    "# 2. save best\n",
    "if not os.path.exists(\"checkpoints\"):\n",
    "    os.makedirs(\"checkpoints\")\n",
    "save_ckpt_callback = SaveCheckpointsCallback(\"checkpoints/monkeys-resnet50\", save_step=len(train_loader), save_best_only=True)\n",
    "# 3. early stop\n",
    "early_stop_callback = EarlyStopCallback(patience=5)\n",
    "\n",
    "model = model.to(device)\n",
    "record = training(\n",
    "    model, \n",
    "    train_loader, \n",
    "    val_loader, \n",
    "    epoch, \n",
    "    loss_fct, \n",
    "    optimizer, \n",
    "    tensorboard_callback=None,\n",
    "    save_ckpt_callback=save_ckpt_callback,\n",
    "    early_stop_callback=early_stop_callback,\n",
    "    eval_step=len(train_loader)\n",
    "    )"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2048\n",
      "1000\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "  0%|          | 0/1380 [00:00<?, ?it/s]"
      ],
      "application/vnd.jupyter.widget-view+json": {
       "version_major": 2,
       "version_minor": 0,
       "model_id": "e0d3a39fdab541ab9c23af5faa9eb31a"
      }
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Early stop at epoch 7 / global_step 483\n"
     ]
    }
   ],
   "execution_count": 17
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "outputs": [
    {
     "data": {
      "text/plain": "'model_architecture.png'"
     },
     "execution_count": 25,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 画图\n",
    "import torch\n",
    "from torchviz import make_dot\n",
    "\n",
    "# Assuming your model is already defined and named 'model'\n",
    "# Construct a dummy input\n",
    "dummy_input = torch.randn(1, 3, 224, 224)  # Replace with your input shape\n",
    "\n",
    "# Forward pass to generate the computation graph\n",
    "output = model(dummy_input)\n",
    "\n",
    "# Visualize the model architecture\n",
    "dot = make_dot(output, params=dict(model.named_parameters()))\n",
    "dot.render(\"model_architecture\", format=\"png\")  # Save the visualization as an image\n"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2024-07-23T03:02:10.300044700Z",
     "start_time": "2024-07-23T03:02:09.443403Z"
    }
   }
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-31T15:10:48.396296Z",
     "start_time": "2025-01-31T15:10:48.265505Z"
    }
   },
   "source": [
    "#画线要注意的是损失是不一定在零到1之间的\n",
    "def plot_learning_curves(record_dict, sample_step=500):\n",
    "    # build DataFrame\n",
    "    train_df = pd.DataFrame(record_dict[\"train\"]).set_index(\"step\").iloc[::sample_step]\n",
    "    val_df = pd.DataFrame(record_dict[\"val\"]).set_index(\"step\")\n",
    "\n",
    "    # plot\n",
    "    fig_num = len(train_df.columns)\n",
    "    fig, axs = plt.subplots(1, fig_num, figsize=(5 * fig_num, 5))\n",
    "    for idx, item in enumerate(train_df.columns):    \n",
    "        axs[idx].plot(train_df.index, train_df[item], label=f\"train_{item}\")\n",
    "        axs[idx].plot(val_df.index, val_df[item], label=f\"val_{item}\")\n",
    "        axs[idx].grid()\n",
    "        axs[idx].legend()\n",
    "        # axs[idx].set_xticks(range(0, train_df.index[-1], 5000))\n",
    "        # axs[idx].set_xticklabels(map(lambda x: f\"{int(x/1000)}k\", range(0, train_df.index[-1], 5000)))\n",
    "        axs[idx].set_xlabel(\"step\")\n",
    "    \n",
    "    plt.show()\n",
    "\n",
    "plot_learning_curves(record, sample_step=10)  #横坐标是 steps"
   ],
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<Figure size 1000x500 with 2 Axes>"
      ],
      "image/png": "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"
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "execution_count": 18
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 评估"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-31T15:11:25.964337Z",
     "start_time": "2025-01-31T15:11:12.134512Z"
    }
   },
   "source": [
    "# dataload for evaluating\n",
    "\n",
    "# load checkpoints\n",
    "model.load_state_dict(torch.load(\"checkpoints/monkeys-resnet50/best.ckpt\", weights_only=True, map_location=\"cpu\"))\n",
    "\n",
    "model.eval()\n",
    "loss, acc = evaluating(model, val_loader, loss_fct)\n",
    "print(f\"loss:     {loss:.4f}\\naccuracy: {acc:.4f}\")"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "loss:     0.3532\n",
      "accuracy: 0.9816\n"
     ]
    }
   ],
   "execution_count": 20
  },
  {
   "metadata": {},
   "cell_type": "code",
   "outputs": [],
   "execution_count": null,
   "source": ""
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "pytorch",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.8"
  },
  "orig_nbformat": 4
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
